import EventKernel from '../kernel/EventKernel';
import AnimateFunction, { AnimateEasing } from './AnimateFunction';
import { bindAll } from '../utils/utils';

interface EventMap {
    start: MapLike;
    update: MapLike;
    complete: MapLike;
    stop: MapLike;
}

class Animator extends EventKernel<EventMap> {
    private _timer?: number;
    private _start?: MapLike;
    private _end?: MapLike;
    private _current: MapLike = {};
    private _startTimestamp: number = 0;
    private _duration: number = 0;
    private _progress: number = 0;
    private _easingFun: Function = AnimateFunction.linear;

    constructor() {
        super();
        bindAll([
            '_updateFrame',
        ], this);
    }

    ofNumber(start: number, end: number, duration: number) {
        this._start = { num: start };
        this._end = { num: end };
        this._duration = duration;

        return this;
    }

    ofObject(start: MapLike, end: MapLike, duration: number) {
        this._start = { ...start };
        this._end = { ...end };
        this._duration = duration;

        return this;
    }

    start() {
        if (this._timer) return;

        this.fire('start', { ...this._start });
        this._current = { ...this._start };

        this.play();

        return this;
    }

    private _updateFrame() {
        if (!this._start || !this._end) return;
        this._progress = (performance.now() - this._startTimestamp) / this._duration;

        if (this._progress >= 1) {
            this._current = { ...this._end };
            this.fire('update', this._current);
            this._complete();
        } else {
            this._timer = requestAnimationFrame(this._updateFrame);

            const percent = this._easingFun(this._progress);
            for (const key in this._current) {
                const step = this._end[key] - this._start[key];
                this._current[key] = this._start[key] + step * percent;
            }
            this.fire('update', this._current);
        }
    }

    pause() {
        this._stopFrameUpdate();
    }

    play() {
        if (this._timer || !this._current || this._progress >= 1) return;
        if (this._duration <= 0) {
            this._current = { ...this._end };
            this.fire('update', this._current);
            this._complete();
        } else {
            this._startTimestamp = performance.now() - this._progress * this._duration;
            this._timer = requestAnimationFrame(this._updateFrame);
        }
    }

    stop() {
        this._stopFrameUpdate();
        this.clearRegister();
        this.fire('stop', { ...this._current });
    }

    private _complete() {
        this._stopFrameUpdate();
        this.fire('complete', this._current);
    }

    private _stopFrameUpdate() {
        this._startTimestamp = 0;
        if (this._timer) {
            cancelAnimationFrame(this._timer);
            delete this._timer;
        }
    }

    easing(name: AnimateEasing) {
        this._easingFun = AnimateFunction[name];

        return this;
    }
}

export default Animator;
